Skip to content

Add local-first Smart Notes dictation#583

Closed
wooboo wants to merge 1 commit into
altic-dev:mainfrom
wooboo:feat/smart-notes
Closed

Add local-first Smart Notes dictation#583
wooboo wants to merge 1 commit into
altic-dev:mainfrom
wooboo:feat/smart-notes

Conversation

@wooboo

@wooboo wooboo commented Jul 11, 2026

Copy link
Copy Markdown

Summary

  • add a configurable global Smart Notes shortcut using the existing recording activation modes
  • save raw transcripts first as human-readable Markdown under Documents/FluidVoice Notes
  • optionally enrich notes with the configured AI provider, adding a title, category, tags, and structured Markdown
  • add a Smart Notes browser with reveal, refresh, and delete actions
  • preserve raw notes and notify the user when AI enrichment fails

Design

SmartNotesStore owns filenames, front matter, atomic writes, reloads, and enrichment updates behind one interface. Note capture is independent from AI availability and bypasses typing, clipboard, prompt-test, and transcription-history routes.

Verification

  • xcodebuild Release build succeeds with code signing disabled
  • standalone storage harness passes raw capture, reload, AI JSON parsing, tag normalization, and in-place enrichment
  • focused SmartNotesStoreTests are included and compile; the local app-hosted XCTest runner stalled while waiting for its target worker before executing them

Copilot AI review requested due to automatic review settings July 11, 2026 12:35
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Description
  • Type of Change
  • Related Issue or Discussion
  • Testing
  • Screenshots / Video

Visual files detected:

  • Sources/Fluid/ContentView.swift
  • Sources/Fluid/Persistence/SettingsStore.swift
  • Sources/Fluid/UI/SettingsView.swift
  • Sources/Fluid/UI/SmartNotesView.swift

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@github-actions github-actions Bot added needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence. labels Jul 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new local-first “Smart Notes” dictation flow that can be triggered globally, persists notes as Markdown under Documents/FluidVoice Notes, and optionally enriches them via the configured AI provider, with an in-app browser to view/manage notes.

Changes:

  • Added SmartNotesStore for capture, atomic Markdown persistence, reload, and AI enrichment updates.
  • Integrated a configurable global “Smart Notes” hotkey and a dedicated output route that bypasses typing/clipboard/history.
  • Added a Smart Notes UI (browser/detail) plus focused store/parser tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Tests/FluidDictationIntegrationTests/SmartNotesStoreTests.swift Adds targeted tests for raw capture, reload, AI response parsing, and in-place enrichment.
Sources/Fluid/UI/SmartNotesView.swift Introduces a Smart Notes browser/detail UI with refresh/reveal/delete actions and AI toggle.
Sources/Fluid/UI/SettingsView.swift Adds Smart Notes shortcut configuration UI using existing shortcut recording patterns.
Sources/Fluid/Services/NotificationService.swift Adds Smart Notes AI-fallback notification plumbing.
Sources/Fluid/Services/GlobalHotkeyManager.swift Adds Smart Notes as a new hotkey-triggerable recording mode across hold/automatic/toggle modes.
Sources/Fluid/Persistence/SmartNotesStore.swift Implements the Smart Notes persistence/enrichment store and Markdown file format.
Sources/Fluid/Persistence/SettingsStore.swift Adds persisted settings for Smart Notes shortcut enablement, shortcut binding, and AI enrichment toggle.
Sources/Fluid/ContentView.swift Wires Smart Notes end-to-end: sidebar, settings bindings, hotkey callbacks, and capture/enrichment stop-path routing.
Fluid.xcodeproj/project.pbxproj Registers the new SmartNotesStoreTests file with the integration test target.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +94 to +95
case .denied:
break
Comment on lines +191 to +195
let content = String(contents[closingRange.upperBound...])
let marker = "<!-- fluidvoice:body -->\n"
let markedBody = content.components(separatedBy: marker).last ?? content
let heading = "# \(title)\n\n"
let body = markedBody.hasPrefix(heading) ? String(markedBody.dropFirst(heading.count)) : markedBody
Comment on lines +265 to +267
private static func fileName(for date: Date, id: UUID) -> String {
"\(self.fileNameDate.string(from: date))-\(id.uuidString.prefix(8).lowercased()).md"
}
Comment on lines +2504 to +2508
/// Smart Notes is opt-in so existing shortcut configurations cannot gain a collision on upgrade.
var smartNotesShortcutEnabled: Bool {
get {
let value = self.defaults.object(forKey: Keys.smartNotesShortcutEnabled)
return value as? Bool ?? false

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39a6e28cf0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +824 to +827
case .toggle:
if self.asrService.isRunning {
if self.isSmartNotesRecordingProvider?() ?? false {
self.stopRecordingIfNeeded()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Debounce Smart Notes toggle key repeats

When Smart Notes is assigned to a normal key chord in Toggle mode, holding the key down generates repeated keyDown events. This branch does not check keyboardEventAutorepeat or latch isSmartNotesKeyPressed, so repeats can invoke triggerSmartNotes multiple times; once ASR starts, the next repeat takes the stop path and can save a truncated note. Please ignore autorepeat/debounce this toggle branch.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a local-first Smart Notes feature: a global hotkey captures voice dictation directly into Markdown files under Documents/FluidVoice Notes, with optional AI enrichment (title, category, tags, structured body), and a new browser pane for viewing, revealing, and deleting notes.

  • SmartNotesStore handles atomic file writes, YAML-style front-matter roundtrip, and in-place AI enrichment; the design correctly bypasses typing, clipboard, and transcription-history routes.
  • The new recording mode integrates cleanly into the existing hotkey manager across all three activation modes and correctly gates the output path before any prompt-test or AI-processing branches.
  • One active display bug: Text(note.body) in the detail pane treats the note body as a plain String, so Markdown formatting from AI-enhanced notes (bullets, emphasis, etc.) appears as raw characters rather than rendered output.

Confidence Score: 3/5

Core recording flow and storage are well-structured, but AI-enhanced notes will display raw Markdown source in the detail view until the Text rendering is fixed.

The Markdown rendering bug directly affects the primary value proposition of AI-enhanced notes — a user who enables 'Enhance with AI' and opens a note will see - item and **bold** as literal characters rather than a formatted list and bold text. This affects every AI-enhanced note in the browser and is immediately visible. The rest of the integration (hotkey wiring, file persistence, settings, notifications) is solid and follows existing patterns, but this display regression in the new view warrants a fix before merge.

Sources/Fluid/UI/SmartNotesView.swift — the noteDetail body rendering; Sources/Fluid/ContentView.swift — the redundant manager update calls in applyShortcut and setShortcutTargetEnabled.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
Sources/Fluid/UI/SmartNotesView.swift:169-173
Markdown body rendered as raw text — `Text(someStringVariable)` in SwiftUI does not interpret Markdown; the initializer that accepts a `String` value skips the Markdown parser. An AI-enhanced note with body `"- Confirm build
- Notify support"` will display the literal hyphens and newlines rather than a rendered list. Using `AttributedString(markdown:)` renders the Markdown correctly.

```suggestion
                    Group {
                        if let attributed = try? AttributedString(markdown: note.body, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
                            Text(attributed)
                        } else {
                            Text(note.body)
                        }
                    }
                    .font(.body)
                    .lineSpacing(5)
                    .textSelection(.enabled)
                    .frame(maxWidth: .infinity, alignment: .leading)
```

### Issue 2 of 3
Sources/Fluid/UI/SmartNotesView.swift:46-50
**Delete error silently swallowed**

`try? self.store.delete(note)` discards any `FileManager.removeItem` error. If the deletion fails (e.g., permissions, file in use), `store.reload()` is still called inside `delete()` and re-reads the file from disk, so the note silently reappears in the list with no feedback. The user sees the confirmation sheet close but the note stays, which is confusing. Consider at minimum logging the error or showing a brief alert.

### Issue 3 of 3
Sources/Fluid/ContentView.swift:1130-1133
**Double manager update on shortcut assignment**

`applyShortcut(.smartNotes)` both writes to `SettingsStore` and calls `hotkeyManager?.updateSmartNotesShortcut(shortcut)` directly. The `onChange(of: smartNotesHotkeyShortcut)` handler (line 485-488) then fires and calls the manager a second time. The same pattern applies to `setShortcutTargetEnabled(.smartNotes)` (line 1181-1184) paired with its `onChange``handleSmartNotesShortcutEnabledChange`, which also calls `updateSmartNotesShortcutEnabled` twice.

The existing `.pasteLast` case avoids this: `applyShortcut(.pasteLast)` only sets state and persists to `SettingsStore`; the manager reads the shortcut live from `SettingsStore` instead of holding its own copy. Either adopt the same read-through approach for Smart Notes, or remove the direct manager calls from `applyShortcut`/`setShortcutTargetEnabled` and let `onChange` remain the single update path.

Reviews (1): Last reviewed commit: "feat: add smart notes dictation" | Re-trigger Greptile

Comment on lines +169 to +173
Text(note.body)
.font(.body)
.lineSpacing(5)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Markdown body rendered as raw text — Text(someStringVariable) in SwiftUI does not interpret Markdown; the initializer that accepts a String value skips the Markdown parser. An AI-enhanced note with body `"- Confirm build

  • Notify support"will display the literal hyphens and newlines rather than a rendered list. UsingAttributedString(markdown:)` renders the Markdown correctly.
Suggested change
Text(note.body)
.font(.body)
.lineSpacing(5)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
Group {
if let attributed = try? AttributedString(markdown: note.body, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
Text(attributed)
} else {
Text(note.body)
}
}
.font(.body)
.lineSpacing(5)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/UI/SmartNotesView.swift
Line: 169-173

Comment:
Markdown body rendered as raw text — `Text(someStringVariable)` in SwiftUI does not interpret Markdown; the initializer that accepts a `String` value skips the Markdown parser. An AI-enhanced note with body `"- Confirm build
- Notify support"` will display the literal hyphens and newlines rather than a rendered list. Using `AttributedString(markdown:)` renders the Markdown correctly.

```suggestion
                    Group {
                        if let attributed = try? AttributedString(markdown: note.body, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
                            Text(attributed)
                        } else {
                            Text(note.body)
                        }
                    }
                    .font(.body)
                    .lineSpacing(5)
                    .textSelection(.enabled)
                    .frame(maxWidth: .infinity, alignment: .leading)
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +46 to +50
Button("Delete Note", role: .destructive) {
guard let note = self.notePendingDeletion else { return }
try? self.store.delete(note)
self.notePendingDeletion = nil
self.selectedNoteID = self.store.notes.first?.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Delete error silently swallowed

try? self.store.delete(note) discards any FileManager.removeItem error. If the deletion fails (e.g., permissions, file in use), store.reload() is still called inside delete() and re-reads the file from disk, so the note silently reappears in the list with no feedback. The user sees the confirmation sheet close but the note stays, which is confusing. Consider at minimum logging the error or showing a brief alert.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/UI/SmartNotesView.swift
Line: 46-50

Comment:
**Delete error silently swallowed**

`try? self.store.delete(note)` discards any `FileManager.removeItem` error. If the deletion fails (e.g., permissions, file in use), `store.reload()` is still called inside `delete()` and re-reads the file from disk, so the note silently reappears in the list with no feedback. The user sees the confirmation sheet close but the note stays, which is confusing. Consider at minimum logging the error or showing a brief alert.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +1130 to +1133
case .smartNotes:
self.smartNotesHotkeyShortcut = shortcut
SettingsStore.shared.smartNotesHotkeyShortcut = shortcut
self.hotkeyManager?.updateSmartNotesShortcut(shortcut)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Double manager update on shortcut assignment

applyShortcut(.smartNotes) both writes to SettingsStore and calls hotkeyManager?.updateSmartNotesShortcut(shortcut) directly. The onChange(of: smartNotesHotkeyShortcut) handler (line 485-488) then fires and calls the manager a second time. The same pattern applies to setShortcutTargetEnabled(.smartNotes) (line 1181-1184) paired with its onChangehandleSmartNotesShortcutEnabledChange, which also calls updateSmartNotesShortcutEnabled twice.

The existing .pasteLast case avoids this: applyShortcut(.pasteLast) only sets state and persists to SettingsStore; the manager reads the shortcut live from SettingsStore instead of holding its own copy. Either adopt the same read-through approach for Smart Notes, or remove the direct manager calls from applyShortcut/setShortcutTargetEnabled and let onChange remain the single update path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/ContentView.swift
Line: 1130-1133

Comment:
**Double manager update on shortcut assignment**

`applyShortcut(.smartNotes)` both writes to `SettingsStore` and calls `hotkeyManager?.updateSmartNotesShortcut(shortcut)` directly. The `onChange(of: smartNotesHotkeyShortcut)` handler (line 485-488) then fires and calls the manager a second time. The same pattern applies to `setShortcutTargetEnabled(.smartNotes)` (line 1181-1184) paired with its `onChange``handleSmartNotesShortcutEnabledChange`, which also calls `updateSmartNotesShortcutEnabled` twice.

The existing `.pasteLast` case avoids this: `applyShortcut(.pasteLast)` only sets state and persists to `SettingsStore`; the manager reads the shortcut live from `SettingsStore` instead of holding its own copy. Either adopt the same read-through approach for Smart Notes, or remove the direct manager calls from `applyShortcut`/`setShortcutTargetEnabled` and let `onChange` remain the single update path.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@wooboo

wooboo commented Jul 11, 2026

Copy link
Copy Markdown
Author

Closed because this change was intended for the fork's main branch, not the upstream repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants